#include "lcd.h"
#include <portmacro.h>
#include <string.h>

// ST7789 初始化命令
static const lcd_init_cmd_t st_init_cmds[] = {
    {0x36, {(1 << 5) | (1 << 6)}, 1},
    {0x3A, {0x55}, 1},
    {0xB2, {0x0c, 0x0c, 0x00, 0x33, 0x33}, 5},
    {0xB7, {0x45}, 1},
    {0xBB, {0x2B}, 1},
    {0xC0, {0x2C}, 1},
    {0xC2, {0x01, 0xff}, 2},
    {0xC3, {0x11}, 1},
    {0xC4, {0x20}, 1},
    {0xC6, {0x0f}, 1},
    {0xD0, {0xA4, 0xA1}, 1},
    {0xE0, {0xD0, 0x00, 0x05, 0x0E, 0x15, 0x0D, 0x37, 0x43, 0x47, 0x09, 0x15, 0x12, 0x16, 0x19}, 14},
    {0xE1, {0xD0, 0x00, 0x05, 0x0D, 0x0C, 0x06, 0x2D, 0x44, 0x40, 0x0E, 0x1C, 0x18, 0x16, 0x19}, 14},
    {0x11, {0}, 0x80},
    {0x29, {0}, 0x80},
    {0, {0}, 0xff}
};

void lcd_cmd(spi_device_handle_t spi, const uint8_t cmd, bool keep_cs_active) {
    esp_err_t ret;

    ret = spi_device_acquire_bus(spi, portMAX_DELAY);
    assert(ret == ESP_OK);

    spi_transaction_t t;
    memset(&t, 0, sizeof(t));
    t.length = 8;
    t.tx_buffer = &cmd;
    t.user = (void*)0;
    if (keep_cs_active) {
        t.flags = SPI_TRANS_CS_KEEP_ACTIVE;
    }
    ret = spi_device_polling_transmit(spi, &t);
    assert(ret == ESP_OK);

    spi_device_release_bus(spi);
}

void lcd_data(spi_device_handle_t spi, const uint8_t *data, int len) {
    esp_err_t ret;
    spi_transaction_t t;
    if (len == 0) {
        return;
    }
    memset(&t, 0, sizeof(t));
    t.length = len * 8;
    t.tx_buffer = data;
    t.user = (void*)1;
    ret = spi_device_polling_transmit(spi, &t);
    assert(ret == ESP_OK);
}

void lcd_spi_pre_transfer_callback(spi_transaction_t *t) {
    int dc = (int)t->user;
    gpio_set_level(PIN_NUM_DC, dc);
}

void lcd_init(spi_device_handle_t spi) {
    int cmd = 0;

    gpio_config_t io_conf = {};
    io_conf.pin_bit_mask = ((1ULL << PIN_NUM_DC) | (1ULL << PIN_NUM_RST));
    io_conf.mode = GPIO_MODE_OUTPUT;
    io_conf.pull_up_en = true;
    gpio_config(&io_conf);

    gpio_set_level(PIN_NUM_RST, 0);
    vTaskDelay(100 / portTICK_PERIOD_MS);
    gpio_set_level(PIN_NUM_RST, 1);
    vTaskDelay(100 / portTICK_PERIOD_MS);

    printf("LCD ST7789V 初始化。\n");
    const lcd_init_cmd_t* lcd_init_cmds = st_init_cmds;

    while (lcd_init_cmds[cmd].databytes != 0xff) {
        lcd_cmd(spi, lcd_init_cmds[cmd].cmd, false);
        lcd_data(spi, lcd_init_cmds[cmd].data, lcd_init_cmds[cmd].databytes & 0x1F);
        if (lcd_init_cmds[cmd].databytes & 0x80) {
            vTaskDelay(100 / portTICK_PERIOD_MS);
        }
        cmd++;
    }
}

void write_one_dot(spi_device_handle_t spi, uint16_t color) {
    uint8_t color_data[] = {
        (uint8_t)(color >> 8),
        (uint8_t)(color & 0xFF)
    };
    lcd_data(spi, color_data, sizeof(color_data));
}

void Lcd_SetBox(spi_device_handle_t spi, int x0, int y0, int x1, int y1) {
    uint8_t col_addr[] = {
        0x2A,
        (uint8_t)(x0 >> 8), (uint8_t)x0,
        (uint8_t)(x1 >> 8), (uint8_t)x1
    };
    lcd_cmd(spi, col_addr[0], true);
    lcd_data(spi, &col_addr[1], sizeof(col_addr) - 1);

    uint8_t row_addr[] = {
        0x2B,
        (uint8_t)(y0 >> 8), (uint8_t)y0,
        (uint8_t)(y1 >> 8), (uint8_t)y1
    };
    lcd_cmd(spi, row_addr[0], true);
    lcd_data(spi, &row_addr[1], sizeof(row_addr) - 1);
}

void draw_pixel(spi_device_handle_t spi, int x, int y, uint16_t color) {
    if (x < 0 || x >= SCREEN_WIDTH || y < 0 || y >= SCREEN_HEIGHT) return;

    uint8_t col_hi = color >> 8;
    uint8_t col_lo = color & 0xFF;

    uint8_t col_addr[] = {
        0x2A,
        0x00, (uint8_t)x,
        0x00, (uint8_t)x
    };
    lcd_cmd(spi, col_addr[0], false);
    lcd_data(spi, &col_addr[1], sizeof(col_addr) - 1);

    uint8_t row_addr[] = {
        0x2B,
        0x00, (uint8_t)y,
        0x00, (uint8_t)y
    };
    lcd_cmd(spi, row_addr[0], false);
    lcd_data(spi, &row_addr[1], sizeof(row_addr) - 1);

    uint8_t ram_wr_cmd = 0x2C;
    lcd_cmd(spi, ram_wr_cmd, false);

    uint8_t color_data[] = { col_hi, col_lo };
    lcd_data(spi, color_data, sizeof(color_data));
}

void draw_char(spi_device_handle_t spi, int x, int y, char c, uint16_t color, uint16_t bgcolor) {
    const uint8_t *bitmap = &Font5x7[(c - ' ') * 5];
    int i, j;

    for (i = 0; i < 5; i++) {
        uint8_t byte = bitmap[i];
        for (j = 0; j < 8; j++) {
            if (byte & (1 << j)) {
                draw_pixel(spi, x + i, y + j, color);
            } else {
                draw_pixel(spi, x + i, y + j, bgcolor);
            }
        }
    }
    for (j = 0; j < 8; j++) {
        draw_pixel(spi, x + 5, y + j, bgcolor);
    }
}

void draw_str(spi_device_handle_t spi, int x, int y, const char *str, uint16_t color, uint16_t bgcolor) {
    while (*str) {
        draw_char(spi, x, y, *str, color, bgcolor);
        x += 6;
        if (x >= SCREEN_WIDTH) {
            x = 0;
            y += 8;
        }
        if (y >= SCREEN_HEIGHT) break;
        str++;
    }
}

void clear_screen(spi_device_handle_t spi, uint16_t color) {
    Lcd_SetBox(spi, 0, 0, SCREEN_WIDTH - 1, SCREEN_HEIGHT - 1);

    lcd_cmd(spi, 0x2C, true);

    for (uint32_t i = 0; i < SCREEN_WIDTH * SCREEN_HEIGHT; i++) {
        write_one_dot(spi, color);
    }
}

void init_spi_bus(void) {
    esp_err_t ret;
    spi_bus_config_t buscfg = {
        .miso_io_num = -1,
        .mosi_io_num = PIN_NUM_MOSI,
        .sclk_io_num = PIN_NUM_CLK,
        .quadwp_io_num = -1,
        .quadhd_io_num = -1,
        .max_transfer_sz = SCREEN_WIDTH * SCREEN_HEIGHT * 2 + 8
    };
    ret = spi_bus_initialize(LCD_HOST, &buscfg, SPI_DMA_CH_AUTO);
    ESP_ERROR_CHECK(ret);
}

spi_device_handle_t init_spi_device(void) {
    esp_err_t ret;
    spi_device_handle_t spi;
    spi_device_interface_config_t devcfg = {
#ifdef CONFIG_LCD_OVERCLOCK
        .clock_speed_hz = 26 * 1000 * 1000,  // 26 MHz
#else
        .clock_speed_hz = 10 * 1000 * 1000,  // 10 MHz
#endif
        .mode = 0,
        .spics_io_num = PIN_NUM_CS,
        .queue_size = 7,
        .pre_cb = lcd_spi_pre_transfer_callback,
    };
    ret = spi_bus_add_device(LCD_HOST, &devcfg, &spi);
    ESP_ERROR_CHECK(ret);
    return spi;
}

void init_lcd(spi_device_handle_t spi) {
    lcd_init(spi);
}